[機械学習] SmartCoreでペンギンの分類をやってみる[Rust]
Introduction
機械学習フレームワークといえば、Tensorflowやscikit-learnが有名ですが、
RustでもlinfaやSmartCoreといった機械学習フレームワークが存在します。
どちらもアクティブに開発されているフレームワークですが、
本稿ではSmartCoreをつかって機械学習してみましょう。
What's my objective?
SmartCoreをつかって機械学習モデルを構築してみます。
対象となるデータですが、今回は「アヤメ(iris)の分類に飽きたらコレ」といわれる、
ペンギンの分類を行います。
これは南極のパーマー群島に住む数種類のペンギン、
- アデリーペンギン(Adelie Penguin)
- ヒゲペンギン(Chinstrap Penguin)
- ジェンツーペンギン(Gentoo Penguin)
について、くちばしやヒレのサイズからペンギンの種類を推論させます。
また、データフレームライブラリにはPolarsを使います。
SmartCore?
SmartCoreはRustの機械学習フレームワークです。
線形代数やらオプティマイザがひととおり用意されており、
いろいろな機械学習アルゴリズムをサポートしています。
ここでもいわれているように、
将来的にRustでMLするときの標準になりそうなフレームワークみたいです。
私としては「sklearn意識度が高い」ってところが良いです。
Polars?
Apache Arrowsをベースにしたデータフレームライブラリです。
PythonとRustで提供されているみたいです。
PythonでデータフレームライブラリといえばPandasですが、
Rustではこれくらいしかない?
参考:
Rustのデータフレームcrateのpolarsとpandasの比較
Environment
- OS : MacOS 10.15.7
- Rust : 1.52.1
Create Rust Example
CargoでProject作成
Cargoでプロジェクトの雛形を作成します。
% cargo new smartcore-example
対象となるペンギンのデータをKaggleからダウンロードします。
ファイルを解凍してpenguins_size.csvをさきほど作成したプロジェクトに移動させておきましょう。
ここのcsvファイルを加工してトレーニングデータとテストデータを作成します。
ちなみにcsvファイル(penguins_size.csv)の内容は↓のようになってます。
species | island | culmen_length_mm | culmen_depth_mm | flipper_length_mm | body_mass_g | sex |
---|---|---|---|---|---|---|
Adelie | Torgersen | 39.1 | 18.7 | 181 | 3750 | MALE |
Adelie | Torgersen | 39.5 | 17.4 | 186 | 3800 | FEMALE |
Adelie | Torgersen | 40.3 | 18 | 195 | 3250 | FEMALE |
Chinstrap | Dream | 50.8 | 19 | 210 | 4100 | MALE |
Chinstrap | Dream | 50.2 | 18.7 | 198 | 3775 | FEMALE |
Gentoo | Biscoe | 46.1 | 13.2 | 211 | 4500 | FEMALE |
Gentoo | Biscoe | 50 | 16.3 | 230 | 5700 | MALE |
polarsとSmartCoreのCrateをCargo.tomlに設定。
・・・ [dependencies] polars = "0.14.7" polars-core = {version = "0.14.7", features=["ndarray"]} smartcore = { version = "0.2.0", default-features = false, features=["nalgebra-bindings", "ndarray-bindings", "datasets"]}
CSVファイルのread
Rustのコードを書いていきます。
まずはcsvファイルをreadします。
下の関数を定義してCSVファイルをDataFrameとして読み込みます。
※ソースコード全文はgistに記載
//CSVファイルを読み込んでDataFrameを返す fn read_csv_with_schema<P: AsRef<Path>>(path: P) -> PolarResult<DataFrame> { let schema = Schema::new(vec![ Field::new("species", DataType::Utf8), Field::new("island", DataType::Utf8), Field::new("culmen_length_mm", DataType::Float64), Field::new("culmen_depth_mm", DataType::Float64), Field::new("flipper_length_mm", DataType::Float64), Field::new("body_mass_g", DataType::Float64), Field::new("sex", DataType::Utf8) ]); let file = File::open(path).expect("Cannot open file."); CsvReader::new(file) .with_schema(Arc::new(schema)) .has_header(true) .with_ignore_parser_errors(true) //エラーが出ても処理継続 .finish() }
Schemaを定義することで、任意の型で各Seriesを定義することがきます。
また、対象のCSVには不正な値を持つデータが存在するので、
with_ignore_parser_errorsを設定して、エラー無視でloadします。
不正なデータを削除
drop_nullsを使えばnullを含むデータを削除できます。
pandasでdf.dropna(how='any')みたいにしてる感じです。
//不正データdrop let df: DataFrame = ・・・ let df2 = df.drop_nulls(None).unwrap();
DataFrameをfeatureとtargetに分割
DataFrameからselectで必要なSeriesを取得し、タプルで返します。
//featureとtargetに分割 fn get_feature_target(df: &DataFrame) -> (PolarResult<DataFrame>, PolarResult<DataFrame>) { let features = df.select(vec![ "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm", "body_mass_g", ]); let target = df.select("species"); (features, target) }
features変換
polarsのDataFrameをSmartCoreのDenseMatrixに変換します。
ほとんどここにある内容まんまです。
pub fn convert_features_to_matrix(df: &DataFrame) -> Result<DenseMatrix<f64>> { let nrows = df.height(); let ncols = df.width(); let features_res = df.to_ndarray::<Float64Type>().unwrap(); let mut xmatrix: DenseMatrix<f64> = BaseMatrix::zeros(nrows, ncols); let mut col: u32 = 0; let mut row: u32 = 0; for val in features_res.iter() { let m_row = usize::try_from(row).unwrap(); let m_col = usize::try_from(col).unwrap(); xmatrix.set(m_row, m_col, *val); if m_col == ncols - 1 { row += 1; col = 0; } else { col += 1; } } Ok(xmatrix) }
Labelエンコーディング
speciesのペンギン名を数値にreplaceします。
(これはもっとまともな方法がある気がする)
//speciesのLabelエンコーディング用function fn str_to_num(str_val: &Series) -> Series { str_val .utf8() .unwrap() .into_iter() .map(|opt_name: Option<&str>| { opt_name.map(|name: &str| { match name { "Adelie" => 1, "Chinstrap" => 2, "Gentoo" => 3, _ => panic!("Problem species str to num"), } }) }) .collect::<UInt32Chunked>() .into_series() } //speciesのLabelエンコーディング let target_array = target .unwrap() .apply("species", str_to_num) .unwrap() .to_ndarray::<Float64Type>() .unwrap(); // create a vec type and populate with y values let mut y: Vec<f64> = Vec::new(); for val in target_array.iter() { y.push(*val); }
学習データとテストデータに分割
smartcoreのtrain_test_splitつかってデータシャッフル&データ分割します。
//データ分割 let (x_train, x_test, y_train, y_test) = train_test_split(&xmatrix.unwrap(), &y, 0.3, true);
学習&推論
そしてトレーニング&テストデータで推論して、
平均二乗誤差と正解率を取得しています。
//学習 let reg = LogisticRegression::fit(&x_train, &y_train, Default::default()).unwrap(); //予測 let preds = reg.predict(&x_test).unwrap(); let mse = mean_squared_error(&y_test, &preds); println!("MSE: {}", mse); println!("accuracy : {}", accuracy(&y_test, &preds));
runで実行してみましょう。
無事に推論できてるみたいです。
% cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.21s Running `target/debug/smartcore-example` MSE: 0.00980392156862745 accuracy : 0.9901960784313726
すべてのコード
ソースコード全文はgistにあります。
Appendix : Python Example
SmartCore + Polarsで実装する前、scikit-learnで試したコード。
from sklearn import datasets from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import Perceptron from sklearn.metrics import accuracy_score import pandas as pd import numpy as np # read csv data = pd.read_csv(f"./penguins_size.csv") # drop NaN data = data.dropna(how='any') # replace str to num target_names = {'Adelie':0,'Chinstrap':1,'Gentoo':2} data['species'] = data['species'].map(target_names) X = data.iloc[:,2:6] y = data.species # split data X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=1, stratify=y) # scale sc = StandardScaler() sc.fit(X_train) X_train_std = sc.transform(X_train) X_test_std = sc.transform(X_test) # fit & predicate ppn = Perceptron(eta0=0.1, random_state=1) ppn.fit(X_train_std, y_train) y_pred = ppn.predict(X_test_std) print('Misclassified examples: %d' % (y_test != y_pred).sum()) print('Accuracy: %.3f' % accuracy_score(y_test, y_pred))
Summary
今回はRustで機械学習ということで、SmartCoreをつかってペンギン分類をおこなってみました。
たしかにSmartCoreはscikit-learnライクでなんとなく使い方はわかる感じです。
それより、Polarを使ったデータ前処理がけっこう手間取ってしまい、
Python+Pandasの手軽さを感じました。
(なれればもっと使いやすく感じるかも?)
次はlinfaもさわってみようかと思います。